/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package pl3;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import pl3.gui.TablesFrame;
import pl3.io.Input;

/**
 * Representates an Context Independent Grammar.
 * Uses a private {@link HashMap HashMap} to store grammar productions.
 * The {@link HashMap HashMap} uses the left side of the production as the <b>key</b> for storing a list of right sides of productions as <b>value</b>. This allows the grammar to have multiple productions with the same left side.
 *
 * @author Luis Alberto Pérez García <luixal@gmail.com>
 * @version 0.2
 */
public class GIC {

    private String id;
    private String initSymbol;
    private HashMap<String, ArrayList<String>> gic;
    // terminals and non-terminals stores:
    private HashSet<String> terminals;
    private HashSet<String> nonTerminals;
    // Syntactic Analisys Table:
    private SyntacticAnalisysTable SAT;
    // Containers for all firstSets and nextSets:
    private HashMap<String, String> firstSets;
    private HashMap<String, String> nextSets;

    /**
     * Default class constructor.
     */
    public GIC() {
        this.id = "GIC";
        this.initSymbol = "";
        this.gic = new HashMap<String, ArrayList<String>>();
        this.terminals = new HashSet<String>();
        this.nonTerminals = new HashSet<String>();
        this.SAT = new SyntacticAnalisysTable("SAT for " + this.id);
        this.firstSets = new HashMap<String, String>();
        this.nextSets = new HashMap<String, String>();
    }

    /**
     * Parametrized class constructor.
     *
     * @param id The ID for the grammar.
     */
    public GIC(String id) {
        this.id = id;
        this.initSymbol = "";
        this.gic = new HashMap<String, ArrayList<String>>();
        this.terminals = new HashSet<String>();
        this.nonTerminals = new HashSet<String>();
        this.SAT = new SyntacticAnalisysTable("SAT for " + this.id);
        this.firstSets = new HashMap<String, String>();
        this.nextSets = new HashMap<String, String>();
    }

    /**
     * Parametrized class constructor.
     *
     * @param id The ID for the grammar.
     * @param initSymbol The init symbol for the grammar.
     * @param filename The file to read the grammar from.
     * @throws IOException
     */
    public GIC(String id, String initSymbol, String filename) throws IOException {
        this.id = id;
        this.initSymbol = initSymbol;
        this.gic = new HashMap<String, ArrayList<String>>(Input.readFile(filename).getGic());
        this.SAT = new SyntacticAnalisysTable("SAT for " + this.id);
        this.firstSets = new HashMap<String, String>();
        this.nextSets = new HashMap<String, String>();
    }

    /**
     * Copy class constructor.
     *
     * @param gic Another GIC Object
     */
    public GIC(GIC gic) {
        this.id = gic.getId();
        this.initSymbol = gic.getInitSymbol();
        this.gic = new HashMap<String, ArrayList<String>>(gic.getGic());
        this.terminals = new HashSet<String>(gic.getTerminals());
        this.nonTerminals = new HashSet<String>(gic.getNonTerminals());
        this.SAT = new SyntacticAnalisysTable(gic.getSAT());
        this.firstSets = new HashMap<String, String>(gic.getFirstSets());
        this.nextSets = new HashMap<String, String>(gic.getNextSets());
    }

    /**
     * Stores a new production into the grammar.
     *
     * @param left Left side of the production
     * @param right Right side of the production
     */
    public void putProduction(String left, String right) {
        if (this.gic.containsKey(left)) {
            this.gic.get(left).add(right);
            this.addSymbols(right);
        } else {
            this.gic.put(left, new ArrayList<String>());
            this.gic.get(left).add(right);
            this.addSymbol(left);
            this.addSymbols(right);
        }
    }

    private void addTerminal(String terminal) {
        this.terminals.add(terminal);
    }

    private void addNonTerminal(String nonTerminal) {
        this.nonTerminals.add(nonTerminal);
    }

    private void addSymbol(String symbol) {
        if (Utils.isTerminal(symbol)) {
            this.addTerminal(symbol);
        } else {
            this.addNonTerminal(symbol);
        }
    }

    private void addSymbols(String input) {
        String[] aux = input.split(" ");
        for (int i = 0; i < aux.length; i++) {
            this.addSymbol(aux[i].trim());
        }
    }

    /**
     * Removes a production from the grammar according to the left side of the production.
     * <b>IMPORTANT:</b> If there are multiple productions sharing the left side <b>all of them will be removed!</b>. A different functionality is provided by {@link GIC#removeProduction(java.lang.String, java.lang.String) another removeProduction} method.
     * @param left Left side of the production
     * @return True if the production was removed. False if it isn't
     */
    public boolean removeProduction(String left) {
        if (this.containsProduction(left)) {
            this.gic.remove(left);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Removes a production from the grammar.
     *
     * @param left Left side of the production
     * @param right Right side of the production
     * @return True if the production was removed. False if it isn't
     */
    public boolean removeProduction(String left, String right) {
        if (this.containsProduction(left, right)) {
            this.gic.get(left).remove(right);
            if (this.gic.get(left).isEmpty()) {
                this.removeProduction(left);
            }
        } else {
            return false;
        }
        return true;
    }

    /**
     * Checks if a production is present in the grammar according to the left side.
     *
     * @param left Left side of the production
     * @return True if the production is in the grammer, False if It isn't
     */
    public boolean containsProduction(String left) {
        return this.gic.containsKey(left);
    }

    /**
     * Checks if a production is present in the grammar.
     *
     * @param left Left side of the production
     * @param right Right side of the production
     * @return True if the production is in the grammer, False if It isn't
     */
    public boolean containsProduction(String left, String right) {
        return (this.gic.containsKey(left) && this.gic.get(left).contains(right));
    }

    /**
     * Retrieves a production from the grammar.
     *
     * @param left Left side of the production
     * @return An empty list if the grammar doesn't contain the production. In other case, the list of right sides related to the supplied left side.
     */
    public ArrayList<String> getProductions(String left) {
        if (this.gic.containsKey(left)) {
            return this.gic.get(left);
        } else {
            return new ArrayList<String>();
        }
    }

    /**
     * Retrieves the set of left parts used as <b>key</b> for indexing productions.
     * 
     * @return Set of left parts contained in the grammar.
     */
    public Set<String> getLefts() {
        return this.gic.keySet();
    }

    /**
     * Retrieves the number of unique left sides productions.
     * <b>IMPORTANT:</b> If you have multiple productions that have equal left sides, they will be counted as one. If you want the <b>total</b> number of productions use the method {@link GIC#realSize() realSize}.
     *
     * @return Number of production with different left sides.
     */
    public Integer size() {
        return this.gic.size();
    }

    /**
     * Retrieves the <b>total</b> number of productions in the grammar.
     *
     * @return Number of productions in the grammar.
     */
    public Integer realSize() {
        Integer r = 0;
        for (String s:this.gic.keySet()) {
            r += 1 * (this.gic.get(s).size());
        }
        return r;
    }

    // getters&setters:

    /**
     * Getter for the ID field.
     *
     * @return The ID of the grammar.
     */
    public String getId() {
        return this.id;
    }

    /**
     * Getter for the initSymbol field.
     *
     * @return The init symbol of the grammar.
     */
    public String getInitSymbol() {
        return this.initSymbol;
    }

    /**
     * Getter for the GIC field.
     *
     * @return A HasMap contaning the grammar.
     */
    protected HashMap<String, ArrayList<String>> getGic() {
        return this.gic;
    }

    /**
     * Getter for the terminals field.
     *
     * @return A set of strings contaning the terminal symbols of the grammar.
     */
    public HashSet<String> getTerminals() {
        return this.terminals;
    }

    /**
     * Getter for the nonTerminals field.
     *
     * @return A set of strings contaning the non-terminal symbols of the grammar.
     */
    public HashSet<String> getNonTerminals() {
        return this.nonTerminals;
    }

    /**
     * Getter for the SAT field.
     *
     * @return The Syntactic Analisys Table of the grammar.
     */
    public SyntacticAnalisysTable getSAT() {
        return this.SAT;
    }

    /**
     * Getter for the firstSets field.
     *
     * @return A HashMap contaning all the firstSets of the grammar.
     */
    public HashMap<String, String> getFirstSets() {
        return this.firstSets;
    }

    /**
     * Getter for the nextSets field.
     *
     * @return A HashMap contaning all the nextSets of the grammar.
     */
    public HashMap<String, String> getNextSets() {
        return this.nextSets;
    }

    /**
     * Setter for the GIC field.
     *
     * @param gic The GIC to be assigned to the grammar.
     */
    protected void setGic(HashMap<String, ArrayList<String>> gic) {
        this.gic = gic;
    }

    /**
     * Setter for the ID field..
     *
     * @param id The ID for the grammar
     */
    public void setId(String id) {
        this.id = id;
    }

    /**
     * Setter for the initSymbol field..
     *
     * @param initSymbol Symbol to set as the init symbol of the grammar
     */
    public void setInitSymbol(String initSymbol) {
        this.initSymbol = initSymbol;
    }

    /**
     * Setter for the terminals field..
     *
     * @param terminals Set of strings to set as the set of terminals of the grammar
     */
    public void setTerminals(HashSet<String> terminals) {
        this.terminals = new HashSet<String>(terminals);
    }

    /**
     * Setter for the nonTerminals field..
     *
     * @param nonTerminals Set of strings to set as the set of non-terminals of the grammar
     */
    public void setNonTerminals(HashSet<String> nonTerminals) {
        this.nonTerminals = new HashSet<String>(nonTerminals);
    }

    /**
     * Setter for the SAT field.
     *
     * @param SAT The Syntactic Analisys Table of the grammar.
     */
    public void setSAT(SyntacticAnalisysTable SAT) {
        this.SAT = new SyntacticAnalisysTable(SAT);
    }

    /**
     * Shows the grammar in a formatted string.
     *
     * @return The string representation of the grammar
     */
    @Override
    public String toString() {
        String result = "";
        result += "Grammar: " + this.id + "\n";
        result += "Terminals: " + this.terminals + "\n";
        result += "Non-Terminals: " + this.nonTerminals + "\n";
        result += "Init Symbol: " + this.initSymbol + "\n";
        result += "Productions: " + this.size() + ", " + this.realSize() + " in total.";

        for (String left:this.gic.keySet()) {
            for (String right:this.getProductions(left)) {
                result += "\n\t" + left + " -> " + right;
            }
        }
        return result;
    }

    ///// algorithms

    /**
     * Calculates the <b>"firstSet"</b> algorithm for the a non-terminal symbol of the grammar.
     *
     * @param x The non-terminal for which the firstSet is calculated
     * @return The firsSet for the symbol x
     */
    public HashSet<String> firstSet(String x) {
        HashSet<String> result = new HashSet<String>();

        // if "x" is terminal --> firstSet(x) = {x}
        if (Utils.isTerminal(x)) {
            result.add(x);
            return result;
        }
        Boolean addLambda = true;
        for (String s:this.getProductions(x)) {
            String y = new StringTokenizer(s).nextToken();
            if (y.equals(Constants.SYMBOLS_LAMBDA)) {
                // if X -> # does exists, then add SYMBOLS_LAMBDA:
                result.add(y);
            } else {
                HashSet<String> firstOfY = new HashSet<String>(firstSet(y));
                if (firstOfY.contains(Constants.SYMBOLS_LAMBDA)) {
                    // if firstSet(Yi) DOES contain SYMBOLS_LAMBDA, then add every symbol EXCEPT SYMBOLS_LAMBDA of firstSet(Yi) to result:
                    firstOfY.remove(Constants.SYMBOLS_LAMBDA);
                } else {
                    addLambda = false;
                }
                // if firstSet(Yi) does NOT contain SYMBOLS_LAMBDA, then add every symbol of firstSet(Yi) to result:
                result.addAll(firstOfY);
            }
        }
        // if SYMBOLS_LAMBDA is contained in every firstSet(Yi), then add SYMBOLS_LAMBDA to result:
        if (addLambda) result.add(Constants.SYMBOLS_LAMBDA);
        return result;
    }

    /**
     * Calculates the <b>"nextSet"</b> algorithm for the a non-terminal symbol of the grammar.
     *
     * @param x The non-terminal for which the nextSet is calculated
     * @return The nextSet for the symbol x
     */
    public HashSet<String> nextSet(String x) {
        HashSet<String> result = new HashSet<String>();
        // if X is the initSymbol, add SYMBOLS_EOL to result:
        if (this.initSymbol.equals(x)) result.add(Constants.SYMBOLS_EOL);
        // if there's a production: A -> y X z, add firstSet(z) - {SYMBOLS_LAMBDA} to result:
        for (String left:this.gic.keySet()) {
            if (!left.equals(x)) {
            for (String right:this.gic.get(left)) {
                ArrayList<String> rightAsList = new ArrayList<String>(Arrays.asList(right.split(" ")));
                if (rightAsList.contains(x)) {
                    if (rightAsList.get(rightAsList.size()-1).equals(x) || this.firstSet(rightAsList.get(rightAsList.size()-1)).contains(Constants.SYMBOLS_LAMBDA)) {
                        result.addAll(this.nextSet(left));
                    }
                    if (!rightAsList.get(rightAsList.size()-1).equals(x)) {
                        HashSet<String> aux = new HashSet<String>(this.firstSet(rightAsList.get(rightAsList.size()-1)));
                        aux.remove(Constants.SYMBOLS_LAMBDA);
                        result.addAll(aux);
                    }
                }
            }
        }
        }
        return result;
    }

    /**
     * Calculates the prediction for a given production.
     *
     * @param left Left side of the production
     * @param right Right side of the production
     * @return A set of strings containing the prediction for the given production.
     */
    public HashSet<String> prediction(String left, String right) {
        if (this.containsProduction(left, right)) {
            HashSet<String> prediction = new HashSet<String>();
            right = right.split(" ")[0];
            // if SYMBOL_LAMBDA belongs to firstOf(right) then return [(firstOf(right) - SYMBOLS_LAMBDA) U nextOf(right)]:
            if (this.firstSet(right).contains(Constants.SYMBOLS_LAMBDA)) {
                prediction = this.firstSet(right);
                prediction.remove(Constants.SYMBOLS_LAMBDA);
                prediction.addAll(this.nextSet(left));
            } else {
                prediction = this.firstSet(right);
            }
            // else return firstOf(right):
            return prediction;
        } else {
            return null;
        }
    }

    /**
     * Algorithm for generating the syntactic analisys table from the grammar.
     */
    public void generateSAT() {
        for (String nt:this.nonTerminals) {
            for (String t:this.terminals) {
                System.out.print("SAT[" + nt + "," + t + "] = ");
                Boolean error = true;
                for (String right:this.getProductions(nt)) {
                    if (this.prediction(nt, right).contains(t)) {
                        this.SAT.put(nt, t, nt + "->" + right);
                        System.out.println(nt + "->" + right);
                        error = false;
                    }
                }
                if (error) this.SAT.put(nt, t, "ERROR");
            }
        }
    }

    /**
     * Generates all the first and next sets for the grammar.
     */
    public void generateSets() {
        for (String left:this.getLefts()) {
            this.firstSets.put(left, this.firstSet(left).toString());
            this.nextSets.put(left, this.nextSet(left).toString());
        }
    }

    /**
     * Evaluates an input file to check it belongs to the grammar.
     *
     * @param filename The name of the file containing the input.
     * @return True if the input belongs to the grammar. Flase any other way.
     * @throws IOException If found any problem reading the file.
     */
    public ArrayList<Boolean> evaluateInputFile(String filename) throws IOException {
        ArrayList<Boolean> booleans = new ArrayList<Boolean>();
        ArrayList<String> inputs = new ArrayList<String>(Input.readInputFile(filename));
        // evaluating inputs...
        TokenProvider tp = this.getTokenProvider();
//        for (String input:inputs) {
            booleans.add(this.evaluateInput(tp));
//        }
        return booleans;
    }

    private TokenProvider getTokenProvider() {
        try {
            // creating lexical analyzer:
            TokenProvider tp = null;
            TokenProviderGenerator.generateTokenProvider("template.lex", "newlexical.lex", this);
            System.out.println("Fichero nuevo generado!");
            System.out.println("Compiling...");
            tp = TokenProviderGenerator.loadTokenProvider("TokenProviderImpl.java");
            tp.yyreset(new java.io.FileReader("input.txt"));
            return tp;
        } catch (FileNotFoundException ex) {
            Logger.getLogger(GIC.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(GIC.class.getName()).log(Level.SEVERE, null, ex);
        } catch (ClassNotFoundException ex) {
                Logger.getLogger(GIC.class.getName()).log(Level.SEVERE, null, ex);
        } catch (InstantiationException ex) {
            Logger.getLogger(GIC.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(GIC.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }
    /**
     * Evaluates an input string to check it belongs to the grammar.
     *
     * @param input The string to ve evaluated.
     * @return True if the string belongs to the grammar. False any other way.
     */
    public boolean evaluateInput(TokenProvider tp) throws IOException {

        Stack<String> stack = new Stack<String>();
//        String[] inputItems;
        // stack init:
        stack.push(Constants.SYMBOLS_EOL);
        stack.push(this.initSymbol);
        // input init:
//        input += " " + Constants.SYMBOLS_EOL;
//        inputItems = input.split(" ");
        String inputItem = tp.yylex();
        // evaluating...
//        Integer pointer = 0; // points to the input item in analisys
//        while (!stack.peek().equals(Constants.SYMBOLS_EOL) && !inputItem.equals(Constants.SYMBOLS_EOL)) {
        while (!stack.peek().equals(Constants.SYMBOLS_EOL)) {
            System.out.print(">>> " + stack + " ::: " + inputItem);
            if (stack.peek().equals(Constants.SYMBOLS_LAMBDA)) {
                stack.pop();
            } else if (stack.peek().equals(inputItem)) {
                // if top of stack and input item are the same, pop it from the stack and remove it from the input:
                stack.pop();
//                pointer++;
                inputItem = tp.yylex();
            } else {
                // if they are different, pop top of stack, then push into the stack the prediction for the top of stack with the input item:
                String prediction = this.SAT.get(stack.peek(), inputItem);
                System.out.println(" >>> " + prediction);
                if (!prediction.equals("ERROR")) {
                    String[] p = prediction.split("->")[1].split(" ");
                    stack.pop();
                    // we need to push it in a reverse orther (algorithm compliancy):
                    for (int i = p.length-1; i >= 0; i--) {
                        stack.push(p[i]);
                    }
                } else {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Main method for testing purpouses
     *
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        HashMap<String, String> firstSets = new HashMap<String, String>();
        HashMap<String, String> nextSets = new HashMap<String, String>();
        System.out.println();
        GIC gic = new GIC(pl3.io.Input.readFile("gic.lex"));
        System.out.println(gic + "\n*****************************************************************");
        for (String p:gic.getLefts()) {
            HashSet<String> f = gic.firstSet(p);
            HashSet<String> n = gic.nextSet(p);
            firstSets.put(p, f.toString());
            nextSets.put(p, n.toString());
            System.out.print(">> FirstOf(" + p + ") = " + f);
            System.out.println("\tNextOf(" + p + ") = " + n);
        }
        System.out.println("*****************************************************************");
        for (String left:gic.getLefts()) {
            for (String right:gic.getProductions(left)) {
                System.out.println("Prediction for: " + left + " -> " + right + " :: " + gic.prediction(left, right));
            }
        }
        // testing SAT generation:
        gic.generateSAT();
        System.out.println("\n\n>>>>>>>>>>>>\n" + gic.getSAT());
        TablesFrame satFrame = new TablesFrame(gic.getSAT(), firstSets, nextSets);
        System.out.print("SAT Frame created, showing... ");
        satFrame.setVisible(true);
        System.out.println("DONE!");
//        GIC gic = new GIC("MyGIC");
//        String s = "ASDF";
//        System.out.println("is " + s + " a terminal? " + Utils.isTerminal(s));
//        s = "ASDF_";
//        System.out.println("is " + s + " a terminal? " + Utils.isTerminal(s));
//        s = "ASDF_ASD";
//        System.out.println("is " + s + " a terminal? " + Utils.isTerminal(s));
//        s = "ASDF_1234";
//        System.out.println("is " + s + " a terminal? " + Utils.isTerminal(s));
//        s = "ASDF_ASDF_134";
//        System.out.println("is " + s + " a terminal? " + Utils.isTerminal(s));
//        s = "_134";
//        System.out.println("is " + s + " a terminal? " + Utils.isTerminal(s));
//        s = "134";
//        System.out.println("is " + s + " a terminal? " + Utils.isTerminal(s));
//        s = "asdfqw_ASDF_134";
//        System.out.println("is " + s + " a terminal? " + Utils.isTerminal(s));
//        System.out.println("\nStoring a grammar for testing...");
//        System.out.println("\t Production for 'E'");gic.putProduction("E", "T RT");
//        System.out.println("\t Production for 'RT'");gic.putProduction("RT", "+ T RT");
//        System.out.println("\t Production for 'RT'");gic.putProduction("RT", "#");
//        System.out.println("\t Production for 'T'");gic.putProduction("T", "F RF");
//        System.out.println("\t Production for 'RF'");gic.putProduction("RF", "F RF");
//        System.out.println("\t Production for 'RF'");gic.putProduction("RF", "#");
//        System.out.println(gic);
//        System.out.println("\nGenerating a copy...");
//        GIC copyGic = new GIC(gic);
//        copyGic.setId("Copy of " + copyGic.getId());
//        System.out.println("Showing the copy...");
//        System.out.println(copyGic);
//        System.out.println("\nDeleting productions for RT");
//        copyGic.removeProduction("RT");
//        System.out.println("Showing:");
//        System.out.println(copyGic);
//        System.out.println("\nDeleting production 'RF -> #'");
//        copyGic.removeProduction("RF", "#");
//        System.out.println("Showing:");
//        System.out.println(copyGic);
//        System.out.println("\nProduction for 'T' -> " + copyGic.getProductions("T").get(0));
    }

}
